import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JProgressBar;

public class XcherServer implements Runnable {
    /*
     * An object of this class represent the work an XCHER Server thread is
     * running. Such a thread is created when this host requests a connection by
     * sending an X_INIT packet to a remote host. XCHER works in a reverse
     * connect mode which basically means that an XCHER Server could be
     * considered a client by the classic definition of client-server
     * relationship. "the host requesting the connection is the client".
     * 
     * Either way, this XCHER Server creates a server socket then sends the
     * connection information in an X_INIT packet to the remote host. Once a
     * connection has been established it begins processing whatever requests
     * the user of the chatclient might have. These can for example be a request
     * of getting the remote hosts filelist. Or to download a file from the
     * remote host.
     */

    private final ArrayList<String> requestedFileList;
    private final LinkedList<String> cmdQueue;
    private boolean support = true;
    private final Pattern regex = Pattern
	    .compile("[^\\s\"']+|\"[^\"]*\"|'[^']*'");
    private BufferedReader input;
    private Socket client;
    private ServerSocket servComSocket;
    // clientNick used for xcherSocket cleanup.
    private String clientNick;
    // chatClient used for gui updates.
    private ChatClient chatClient;
    // saves download threads. So we can cancel them.
    private final LinkedList<Thread> threadsList;
    private boolean Initialized;

    public XcherServer(String nick, ChatClient chatClient) {
	requestedFileList = new ArrayList<String>();
	threadsList = new LinkedList<Thread>();
	Initialized = false;
	cmdQueue = new LinkedList<String>();

	try {
	    this.chatClient = chatClient;
	    clientNick = nick;
	    servComSocket = new ServerSocket(0);
	} catch (IOException e) {
	    e.printStackTrace();
	}
    }

    public ServerSocket getServComSocket() {
	return servComSocket;
    }

    public boolean isSupport() {
	/*
	 * used to check if the remote host this server attempted to establish a
	 * connection with was marked as not supporting XCHER.
	 */
	return support;
    }

    public boolean hasFileList() {
	/*
	 * used to check if this server has ever received the filelist of the
	 * connected remote host.
	 */
	return Initialized;
    }

    public void addToQueue(String cmd) {
	/* add a command to the workqueue. */
	cmdQueue.push(cmd);
    }

    public Object[] getRequestedFileList() {
	return requestedFileList.toArray();
    }

    @Override
    public void run() {

	try {
	    servComSocket.setSoTimeout(10000);
	    client = servComSocket.accept();

	    input = new BufferedReader(new InputStreamReader(
		    client.getInputStream(), "UTF-8"));

	} catch (Exception e1) {
	    // if no client connects within 10s we can assume it does not
	    // support XCHER and close the thread. No more connections will be
	    // attempted to this host.
	    support = false;
	    try {
		servComSocket.close();
	    } catch (IOException e) {
		return;
	    }
	    return;
	}

	while (true) {

	    if (!cmdQueue.isEmpty()) {
		String message = cmdQueue.poll();

		if (message.startsWith("X_CHK")) {
		    actCheck(message);
		} else if (message.startsWith("X_FILELIST")) {
		    actFileList();
		} else if (message.startsWith("X_REQUEST")) {
		    actRequest(message);
		} else if (message.startsWith("X_END")) {
		    if (!actEnd()) {
			break;
		    }
		}

	    } else {
		try {
		    Thread.sleep(1000);
		} catch (InterruptedException e) {
		    e.printStackTrace();
		}
	    }
	}
    }

    public void disconnect() {
	/*
	 * This method is called to shutdown the XCHER session.
	 */
	try {
	    // interrupt and shutdown all currently running downloads from this
	    // user.
	    for (Thread i : threadsList) {
		i.interrupt();
	    }
	    servComSocket.close();
	    client.close();
	} catch (IOException e) {
	    e.printStackTrace();
	}
    }

    private boolean actEnd() {
	/*
	 * This method runs when the user running the XCHER server has requested
	 * to kill all connections to remote host. What it basically does is
	 * alert the remote host then continues to close all sockets to it.
	 * Finally it removes this server work and exists the thread.
	 */
	try {
	    client.getOutputStream().write(("X_END\r\n").getBytes("UTF-8"));
	} catch (IOException e) {
	    e.printStackTrace();
	}

	// disconnect the Com. socket.
	disconnect();
	// remove this connection from the connections list.
	chatClient.getXcherSocket().remove(clientNick);

	// returning false breaks the while loop outside, which results in a
	// thread shutdown.
	return false;
    }

    private void actRequest(String message) {
	/*
	 * The user running this server requested a file to be downloaded from
	 * remote host. What this method does is start FileReceiver thread and
	 * tell the remote host to connect to the FileReceiver and start sending
	 * the file data onto its socket.
	 */
	try {
	    ArrayList<String> chop = inputChop(message);

	    ServerSocket fileServerSocket = new ServerSocket(0);
	    FileReceiver fr = new FileReceiver(chop.get(1).replace("\"", ""),
		    chop.get(2), fileServerSocket);
	    Thread temp = new Thread(fr);
	    temp.start();
	    // add to list of threads which is used for canceling running
	    // downloads.
	    threadsList.add(temp);
	    // tell remote host a file download is requested.
	    client.getOutputStream().write(
		    (message + " " + fileServerSocket.getLocalPort() + "\r\n")
			    .getBytes("UTF-8"));

	} catch (IOException e) {
	    e.printStackTrace();
	}

    }

    private boolean actFileList() {
	/*
	 * The user running this server requested a filelist from the remote
	 * host associated with this server. The function of this method is to
	 * send the X_FILELIST request to remote host then wait for each file
	 * information sent back by the remote host running an XCHER Client. It
	 * loops until EOF EOF message is received which indicates that the
	 * remote host has iterated through all of his shared files.
	 */

	try {
	    client.getOutputStream()
		    .write(("X_FILELIST\r\n").getBytes("UTF-8"));

	    requestedFileList.clear();
	    while (true) {
		String msg = input.readLine();
		ArrayList<String> aTmp = inputChop(msg);
		if (aTmp.get((aTmp.size() - 1)).equals("EOF")) {
		    break;
		}
		requestedFileList.add(aTmp.get(1) + " " + aTmp.get(2));

	    }
	    Initialized = true;

	    return true;
	} catch (IOException e) {
	    e.printStackTrace();
	}

	return false;
    }

    private boolean actCheck(String message) {
	/*
	 * The user running this server requested a check for a specified
	 * filename on the remote host. This usually means the user wants to
	 * know whether the remote host associated with this XCHER server has a
	 * shared file with the same name as in the message.
	 */

	try {
	    client.getOutputStream()
		    .write((message + "\r\n").getBytes("UTF-8"));

	    String msg = input.readLine();

	    if (msg.endsWith("YES")) {
		chatClient.getGui().addXcherSearchResult(
			clientNick + " " + msg.replace("X_CHK", ""));
		return true;
	    }
	    // file don't exist do nothing.
	} catch (Exception e) {
	    e.printStackTrace();
	}
	return false;

    }

    public ArrayList<String> inputChop(String msg) {
	/*
	 * Helper method for splitting input message strings.
	 */
	ArrayList<String> aTmp = new ArrayList<String>();

	Matcher regexMatcher = regex.matcher(msg);
	while (regexMatcher.find()) {
	    aTmp.add(regexMatcher.group());
	}
	return aTmp;
    }

    private class FileReceiver implements Runnable {
	/*
	 * This is the work carried out by the FileReceiver threads. All it does
	 * is read the file data received on the socket and then writes the data
	 * to a file. Lastly it updates the upload progress bars on the GUI of
	 * this users chatclient.
	 */

	private final String filename;
	private final int size;
	private final ServerSocket servDataSocket;
	private final JProgressBar pbar;

	public FileReceiver(String filename, String size,
		ServerSocket fileServerSocket) {
	    pbar = chatClient.getGui().addNewDownloadProgressBar(filename);
	    this.filename = filename;
	    this.size = Integer.parseInt(size);
	    servDataSocket = fileServerSocket;
	}

	@Override
	public void run() {
	    File file = null;
	    Socket receiveSocket = null;
	    FileOutputStream out = null;
	    try {

		receiveSocket = servDataSocket.accept();

		InputStream input = receiveSocket.getInputStream();
		// the folder to put downloads in.
		File theDir = new File("Downloads");

		// if the directory does not exist, create it
		if (!theDir.exists()) {
		    theDir.mkdir();
		}

		file = new File("Downloads/" + filename);
		out = new FileOutputStream(file);

		byte[] buffer = new byte[16 * 1024];

		float bytesReceivedTotal = 0;
		int bytesReceived = 0;

		while (((bytesReceived = input.read(buffer)) > 0)
			&& !Thread.interrupted()) {
		    out.write(buffer, 0, bytesReceived);
		    bytesReceivedTotal += bytesReceived;
		    // sets the new progress value.
		    pbar.setValue((int) ((bytesReceivedTotal / size) * 100));
		}
		chatClient.getGui().removeDownloadProgressBar(pbar);
		receiveSocket.close();
		out.close();
	    } catch (Exception e) {
		try {
		    out.close();
		    receiveSocket.close();
		} catch (Exception e1) {
		}
		chatClient.getGui().removeDownloadProgressBar(pbar);
	    }

	}
    }

}
